/* eslint-disable no-throw-literal */
const _ = require('lodash')
const { ObjectID } = require('mongodb')
const { models } = require('../../model')
const resourceVersionCollection = models.EditLog.coll

const utimeUpdate = eventRecord => ({ ...eventRecord, ...{ utime: new Date() } })
const stimeUpdate = eventRecord => ({ ...eventRecord, ...{ stime: new Date() } })
const rtimeUpdate = eventRecord => ({ ...eventRecord, ...{ rtime: new Date() } })
const dataUpdate = (eventRecord, { data }) => ({ ...eventRecord, ...{ data: _.cloneDeep(data) } })
const editorUpdate = (eventRecord, { user }) => ({ ...eventRecord, ...{ editorId: user._id, editorName: user.name } })
const reviewerUpdate = (eventRecord, { user }) => ({
  ...eventRecord,
...{
    reviewer_id: user._id,
    reviewerName: user.name
  }
})
const defaultProcesses = { utimeUpdate, stimeUpdate, rtimeUpdate, dataUpdate, editorUpdate, reviewerUpdate }

const defaultActivities = [
  {
    name: 'stashed',
    gateway: {
      'stashed -> editing': [editorUpdate, dataUpdate, utimeUpdate]
    }
  },
  {
    name: 'editing',
    gateway: {
      'editing -> editing': [dataUpdate, utimeUpdate],
      'editing -> committed': [utimeUpdate, stimeUpdate],
      'editing -> abandoned': utimeUpdate,
      'editing -> finished': [utimeUpdate, stimeUpdate, rtimeUpdate, reviewerUpdate]
    }
  },
  {
    name: 'committed',
    gateway: {
      'committed -> reviewing': [utimeUpdate, reviewerUpdate],
      'committed -> abandoned': utimeUpdate,
      'committed -> editing': utimeUpdate
    }
  },
  {
    name: 'reviewing',
    gateway: {
      'reviewing -> finished': [utimeUpdate, rtimeUpdate, function (eventRecord, { data }) {
        // 审核者修正流程
        if (_.isObject(data.correction)) {
          eventRecord.data = data.correction
          eventRecord.correction_comment = data.correction_comment || ''
        }
        return eventRecord
      }],
      'reviewing -> rejected': [utimeUpdate, rtimeUpdate, function (eventRecord, { data }) {
        return {
          ...eventRecord,
          ...{
            reviewerComment: data.reviewerComment
          }
        }
      }],
      // 修正保存流程
      'reviewing -> reviewing': [utimeUpdate, rtimeUpdate, function (eventRecord, { data }) {
        // 审核者修正流程
        if (_.isObject(data.correction)) {
          eventRecord.data = data.correction
        }
        return eventRecord
      }]
    }
  },
  {
    name: 'rejected',
    gateway: {
      'rejected -> editing': utimeUpdate
    }
  },
  {
    name: 'finished',
    gateway: {}
  },
  {
    name: 'abandoned',
    gateway: {}
  }
]

const defaultOperations = {
  stash: 'stashed -> editing',
  setData: 'editing -> editing',
  commit: 'editing -> committed',
  edit: ['committed -> editing', 'rejected -> editing'],
  cancel: 'editing -> abandoned',
  review: 'committed -> reviewing',
  correct: 'reviewing -> reviewing',
  pass: ['reviewing -> finished', 'editing -> finished'],
  reject: 'reviewing -> rejected'
}

class WorkFlow {
  /**
   * 构造函数
   * @param {object} options
   * @param {Collection} [options.collection=]    - 工作流记录 collection
   * @param {*} [options.name=]                   - 工作流事件名称
   * @param {*} [options.event=]                  - 事件基础 event
   * @param {*} [options.activities=]             - 活动配置
   * @param {*} [options.operations=]             - 流转操作配置
   */
  constructor (options) {
    // 工作流记录 collection
    this.collection = options.collection || resourceVersionCollection
    // event - activity - gateway - flow
    this.event = options.event || {}
    this.event.type = options.name || 'normal'
    this.activities = options.activities || _.cloneDeep(defaultActivities)
    this.operations = options.operations || _.cloneDeep(defaultOperations)
    this.afterCreateHooks = []
    this.afterRecordProcessHooks = []
    /** @type {{process:{}}|{operation:{}}|{activity:{}}|*} */
    this.default = new Proxy({}, {
      get: function (target, p, receiver) {
        if (p === 'activity') return defaultActivities
        if (p === 'operation') return defaultOperations
        if (p === 'process') return defaultProcesses
        return receiver
      }
    })
    this.workflowProperties = [
      'status', 'ctime', 'utime', 'stime', 'rtime',
      'editorName', 'reviewerName', 'reviewerComment', 'editType'
    ]
    this.eidtTypes = [
      'create', 'update', 'delete', 'publish', 'reject'
    ]
  }

  async _eventProcess (eventRecord, operationName, { user, data, rawEventRecord }) {
    const activity = _.find(this.activities, e => e.name === eventRecord.status)
    let flows = this.operations[operationName]
    flows = _.isArray(flows) ? flows : [flows]
    const flow = _.find(flows, flow => activity.gateway[flow])
    let flowProcesses = activity.gateway[flow]
    if (!flowProcesses) {
      throw `当前记录状态为【${WorkFlow._getStatusText(eventRecord.status)}】` +
      `不可进行【${WorkFlow._getOperationText(operationName)}】操作`
    }
    if (!_.isArray(flowProcesses)) {
      flowProcesses = [flowProcesses]
    }
    // activity flow
    const [, from, to] = (flow.match(/(.*) -> (.*)/) || [])
    if (!from || !to) throw `异常流转过程 ${operationName}`
    eventRecord.status = to
    // flow process
    if (_.isFunction(flowProcesses)) {
      flowProcesses = [flowProcesses]
    }
    flowProcesses = flowProcesses.filter(e => _.isFunction(e))
    let curEvent = eventRecord
    for (let i = 0; i < flowProcesses.length; i++) {
      curEvent = await flowProcesses[i](curEvent, { user, data, rawEventRecord })
    }
    return curEvent
  }

  /**
   * 工作流创建
   * @param {*} [user=]           - 操作用户
   * @param {*} [data=]           - 操作数据
   * @return {Promise<*|ObjectId>}
   */
  async create ({ user, data }) {
    // 工作流记录原型
    let eventRecord = _.cloneDeep(this.event)
    const activity = this.activities[0]
    eventRecord = {
      ...eventRecord,
      // 工作流记录基础数据
      ...{
        _id: new ObjectID(),
        status: activity.name,
        ctime: new Date(),
        utime: new Date(),
        editorId: user._id,
        editorName: user.name
      },
      // 工作流记录操作数据
      ...(data ? { data } : {})
    }
    const curEventRecord = await this._eventProcess(eventRecord, 'stash', { user, data })
    const result = await this.collection.insertOne(curEventRecord)
    // 工作流记录插入数据库之后附加 _id
    eventRecord._id = result.insertedId
    this.afterCreateHooks.forEach(cb => cb(eventRecord, { user, data }))
    return result.insertedId
  }

  /**
   * 工作流活动状态流转
   * @param {*} id                - 工作流事件记录 ID
   * @param {string} operation    - 操作类型
   * @param {*} [user=]           - 操作用户
   * @param {*} [data=]           - 操作数据
   * @return {Promise<*>}
   */
  async update (id, operation, { user, data }) {
    const eventRecord = await this.collection.findOne({ _id: ObjectID(id) })
    if (!eventRecord) throw `无对应 id ${id} 工作流记录`
    const curEventRecord = await this._eventProcess(eventRecord, operation, {
      user,
      data,
      rawEventRecord: _.cloneDeep(eventRecord)
    })
    await this.collection.updateOne({ _id: ObjectID(id) }, { $set: curEventRecord })
    return id
  }

  static _getStatusText (status) {
    return {
      editing: '编辑中',
      committed: '待审核',
      reviewing: '审核中',
      finished: '已通过',
      rejected: '未通过',
      abandoned: '已删除'
    }[status] || status
  }

  static _getOperationText (operationName) {
    return {
      setData: '更新编辑',
      commit: '提交',
      edit: '撤回编辑',
      cancel: '删除',
      review: '审核',
      pass: '审核通过',
      reject: '审核拒绝'
    }[operationName] || operationName
  }

  static _getEditType (editType) {
    return {
      edit: '修改',
      create: '新增'
    }[editType] || editType
  }

  async _recordProcess (rawRecord) {
    const record = _.cloneDeep(rawRecord.data)
    rawRecord.status = WorkFlow._getStatusText(rawRecord.status)
    rawRecord.resourceId = rawRecord.data.id
    for (const afterRecordProcessHook of this.afterRecordProcessHooks) {
      await afterRecordProcessHook(record, rawRecord)
    }
    rawRecord.data = record
    return rawRecord
  }

  /**
   * 获取工作流记录列表
   * @param {string|Array} status     - 状态筛选
   * @param {number} [offset=0]       - 分页偏移
   * @param {number} [limit=10]       - 分页大小
   * @param {*} [cond={}]             - 自定义筛选条件
   * @param {*} [sort={utime: -1}]    - 排序
   * @param {*} [user]                - 用户信息
   * @param {*} [type=]               - 列表类型 edit review
   * @return {Promise<{total, rows: *}>}
   */
  async getList (status, { cond, offset, limit, sort, user, type }) {
    // 'unreviewed': 'committed',
    // 'reviewed': ['finished', 'rejected'],
    if (!status) throw `工作流列表筛选状态异常 ${status}`
    offset = +offset || 0
    limit = +limit || 10
    // 编辑流程 仅编辑者可见
    // 审核流程 待审核全部可见 其它状态当前审核者可见
    const userFilter = {}
    if (type === 'edit') {
      userFilter.editorId = user._id
    }
    if (type === 'review') {
      if (status !== 'committed') {
        userFilter.reviewer_id = user._id
      }
    }
    // 汇总过滤条件
    cond = {
      type: this.event.type,
      status: _.isArray(status) ? { $in: status } : status,
      ...userFilter,
      ...(cond || {})
    }
    let total = await this.collection.countDocuments(cond)
    let rows = await this.collection.find(cond).sort({
      ...{ utime: -1 },
      ...(sort || {})
    }).skip(offset).limit(limit).toArray()
    rows = await Promise.all(rows.map(record => this._recordProcess(record)))
    if (rows.length <= limit && total <= limit && total !== rows.length) total = rows.length
    return { total: total, rows: rows }
  }

  /**
   * 获取工作流事件记录详情
   * @param {string} id    - 工作流记录 ID
   * @return {Promise<*>}
   */
  async getDetail (id) {
    const record = await this.collection.findOne({ _id: ObjectID(id) })
    return this._recordProcess(record)
  }

  _getGatewayByFlow (flow) {
    const gateways = this.activities.map(e => e.gateway)
    const gateway = _.find(gateways, gateway => gateway[flow])
    if (!gateway) throw 'flow not found'
    return gateway
  }

  /**
   * 流转过程处理回调函数
   * @callback ProcessCallback
   * @param {*} eventRecord                   - 操作记录
   * @param {{data?:Object,user?:Object}} data  - 附加数据
   */

  /**
   * 重新配置流转过程的处理过程
   * @param {string} flow                 - 流转过程
   * @param {Array<ProcessCallback>} processes   - 处理过程
   */
  setFlowProcesses (flow, processes) {
    const gateway = this._getGatewayByFlow(flow)
    gateway[flow] = processes
  }

  /**
   * 流转过程添加处理过程
   * @param {string} flow      - 流转过程
   * @param {ProcessCallback} process   - 处理过程
   */
  addFlowProcess (flow, process) {
    const gateway = this._getGatewayByFlow(flow)
    if (!_.isArray(gateway[flow])) gateway[flow] = [gateway[flow]]
    gateway[flow].push(process)
  }

  /**
   * 针对单个流转过程添加多个处理过程
   * @param {string} flow    - 流转过程
   * @param {Array<ProcessCallback>} processes - 处理过程
   */
  addFlowProcesses (flow, processes) {
    const gateway = this._getGatewayByFlow(flow)
    if (!_.isArray(gateway[flow])) gateway[flow] = [gateway[flow]]
    gateway[flow] = gateway[flow].concat(processes)
  }

  /**
   * 针对多个流转过程添加处理过程
   * @param {array} flows             - 流转过程
   * @param {ProcessCallback} process - 处理过程
   */
  addFlowsProcess (flows, process) {
    flows.forEach(flow => {
      this.addFlowProcess(flow, process)
    })
  }

  /**
   * 记录创建后钩子函数
   * 此时已经新记录已写入数据库，记录数据包含 _id 字段
   * @param {ProcessCallback} cb      - 回调函数
   */
  afterCreate (cb) {
    this.afterCreateHooks.push(cb)
  }

  /**
   * 记录加工后钩子函数
   * @param {ProcessCallback} cb
   */
  afterRecordProcess (cb) {
    this.afterRecordProcessHooks.push(cb)
  }

  /**
   * 添加流转过程
   * @param {string} activityName
   * @param {string} flowName
   * @param {array|function} flowProcesses
   */
  addFlow (activityName, flowName, flowProcesses) {
    const activity = _.find(this.activities, activity => activity.name === activityName)
    if (activity.gateway[flowName]) throw 'workflow.addFlow fail , same name gateway already exists'
    activity.gateway[flowName] = flowProcesses
  }
}

module.exports = WorkFlow
